package ar.com.hjg.pngj.pixels;

import java.util.Arrays;

import ar.com.hjg.pngj.FilterType;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjExceptionInternal;

/** for use in adaptative strategy */
public class FiltersPerformance {

  private final ImageInfo iminfo;
  private double memoryA = 0.7; // empirical (not very critical: 0.72)
  private int lastrow = -1;
  private double[] absum = new double[5];// depending on the strategy not all values might be
                                         // computed for all
  private double[] entropy = new double[5];
  private double[] cost = new double[5];
  private int[] histog = new int[256]; // temporary, not normalized
  private int lastprefered = -1;
  private boolean initdone = false;
  private double preferenceForNone = 1.0; // higher gives more preference to NONE

  // this values are empirical (montecarlo), for RGB8 images with entropy estimator for NONE and
  // memory=0.7
  // DONT MODIFY THIS
  public static final double[] FILTER_WEIGHTS_DEFAULT = {0.73, 1.03, 0.97, 1.11, 1.22}; // lower is
                                                                                        // better!

  private double[] filter_weights = new double[] {-1, -1, -1, -1, -1};

  private final static double LOG2NI = -1.0 / Math.log(2.0);

  public FiltersPerformance(ImageInfo imgInfo) {
    this.iminfo = imgInfo;
  }

  private void init() {
    if (filter_weights[0] < 0) {// has not been set from outside
      System.arraycopy(FILTER_WEIGHTS_DEFAULT, 0, filter_weights, 0, 5);
      double wNone = filter_weights[0];
      if (iminfo.bitDepth == 16)
        wNone = 1.2;
      else if (iminfo.alpha)
        wNone = 0.8;
      else if (iminfo.indexed || iminfo.bitDepth < 8)
        wNone = 0.4; // we prefer NONE strongly
      wNone /= preferenceForNone;
      filter_weights[0] = wNone;
    }
    Arrays.fill(cost, 1.0);
    initdone = true;
  }

  public void updateFromFiltered(FilterType ftype, byte[] rowff, int rown) {
    updateFromRawOrFiltered(ftype, rowff, null, null, rown);
  }

  /** alternative: computes statistic without filtering */
  public void updateFromRaw(FilterType ftype, byte[] rowb, byte[] rowbprev, int rown) {
    updateFromRawOrFiltered(ftype, null, rowb, rowbprev, rown);
  }

  private void updateFromRawOrFiltered(FilterType ftype, byte[] rowff, byte[] rowb,
      byte[] rowbprev, int rown) {
    if (!initdone)
      init();
    if (rown != lastrow) {
      Arrays.fill(absum, Double.NaN);
      Arrays.fill(entropy, Double.NaN);
    }
    lastrow = rown;
    if (rowff != null)
      computeHistogram(rowff);
    else
      computeHistogramForFilter(ftype, rowb, rowbprev);
    if (ftype == FilterType.FILTER_NONE)
      entropy[ftype.val] = computeEntropyFromHistogram();
    else
      absum[ftype.val] = computeAbsFromHistogram();
  }

  /* WARNING: this is not idempotent, call it just once per cycle (sigh) */
  public FilterType getPreferred() {
    int fi = 0;
    double vali = Double.MAX_VALUE, val = 0; // lower wins
    for (int i = 0; i < 5; i++) {
      if (!Double.isNaN(absum[i])) {
        val = absum[i];
      } else if (!Double.isNaN(entropy[i])) {
        val = (Math.pow(2.0, entropy[i]) - 1.0) * 0.5;
      } else
        continue;
      val *= filter_weights[i];
      val = cost[i] * memoryA + (1 - memoryA) * val;
      cost[i] = val;
      if (val < vali) {
        vali = val;
        fi = i;
      }
    }
    lastprefered = fi;
    return FilterType.getByVal(lastprefered);
  }

  public final void computeHistogramForFilter(FilterType filterType, byte[] rowb, byte[] rowbprev) {
    Arrays.fill(histog, 0);
    int i, j, imax = iminfo.bytesPerRow;
    switch (filterType) {
      case FILTER_NONE:
        for (i = 1; i <= imax; i++)
          histog[rowb[i] & 0xFF]++;
        break;
      case FILTER_PAETH:
        for (i = 1; i <= imax; i++)
          histog[PngHelperInternal.filterRowPaeth(rowb[i], 0, rowbprev[i] & 0xFF, 0)]++;
        for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++)
          histog[PngHelperInternal.filterRowPaeth(rowb[i], rowb[j] & 0xFF, rowbprev[i] & 0xFF,
              rowbprev[j] & 0xFF)]++;
        break;
      case FILTER_SUB:
        for (i = 1; i <= iminfo.bytesPixel; i++)
          histog[rowb[i] & 0xFF]++;
        for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++)
          histog[(rowb[i] - rowb[j]) & 0xFF]++;
        break;
      case FILTER_UP:
        for (i = 1; i <= iminfo.bytesPerRow; i++)
          histog[(rowb[i] - rowbprev[i]) & 0xFF]++;
        break;
      case FILTER_AVERAGE:
        for (i = 1; i <= iminfo.bytesPixel; i++)
          histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF)) / 2) & 0xFF]++;
        for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++)
          histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF) + (rowb[j] & 0xFF)) / 2) & 0xFF]++;
        break;
      default:
        throw new PngjExceptionInternal("Bad filter:" + filterType);
    }
  }

  public void computeHistogram(byte[] rowff) {
    Arrays.fill(histog, 0);
    for (int i = 1; i < iminfo.bytesPerRow; i++)
      histog[rowff[i] & 0xFF]++;
  }

  public double computeAbsFromHistogram() {
    int s = 0;
    for (int i = 1; i < 128; i++)
      s += histog[i] * i;
    for (int i = 128, j = 128; j > 0; i++, j--)
      s += histog[i] * j;
    return s / (double) iminfo.bytesPerRow;
  }

  public final double computeEntropyFromHistogram() {
    double s = 1.0 / iminfo.bytesPerRow;
    double ls = Math.log(s);

    double h = 0;
    for (int x : histog) {
      if (x > 0)
        h += (Math.log(x) + ls) * x;
    }
    h *= s * LOG2NI;
    if (h < 0.0)
      h = 0.0;
    return h;
  }

  /**
   * If larger than 1.0, NONE will be more prefered. This must be called before init
   * 
   * @param preferenceForNone around 1.0 (default: 1.0)
   */
  public void setPreferenceForNone(double preferenceForNone) {
    this.preferenceForNone = preferenceForNone;
  }

  /**
   * Values greater than 1.0 (towards infinite) increase the memory towards 1. Values smaller than 1.0 (towards zero) decreases the memory .
   * 
   */
  public void tuneMemory(double m) {
    if (m == 0)
      memoryA = 0.0;
    else
      memoryA = Math.pow(memoryA, 1.0 / m);
  }

  /**
   * To set manually the filter weights. This is not recommended, unless you know what you are doing. Setting this ignores preferenceForNone and omits some heuristics
   * 
   * @param weights Five doubles around 1.0, one for each filter type. Lower is preferered
   */
  public void setFilterWeights(double[] weights) {
    System.arraycopy(weights, 0, filter_weights, 0, 5);
  }
}
